//BeanCipher - Bean Cipher Security Layer
//Copyright (C) 2005 Raul Alfredo Bajales
//
//This library is free software; you can redistribute it and/or
//modify it under the terms of the GNU Lesser General Public
//License as published by the Free Software Foundation; either
//version 2.1 of the License, or (at your option) any later version.
//
//This library is distributed in the hope that it will be useful,
//but WITHOUT ANY WARRANTY; without even the implied warranty of
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
//Lesser General Public License for more details.
//
//You should have received a copy of the GNU Lesser General Public
//License along with this library; if not, write to the Free Software
//Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package ar.com.rab.beancipher.impl.configuration;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.sadun.util.SimpleClassPackageExplorer;

import ar.com.rab.beancipher.impl.PropertyCipher;
import ar.com.rab.beancipher.impl.configuration.model.beanmapping.CFGAllProperties;
import ar.com.rab.beancipher.impl.configuration.model.beanmapping.CFGBeanMapping;
import ar.com.rab.beancipher.impl.configuration.model.beanmapping.CFGMapping;
import ar.com.rab.beancipher.impl.configuration.model.beanmapping.CFGProperty;
import ar.com.rab.beancipher.impl.configuration.model.registry.CFGPropertyCipher;
import ar.com.rab.beancipher.impl.configuration.model.registry.CFGRegistry;
import ar.com.rab.beancipher.impl.configuration.model.registry.CFGWhen;
import ar.com.rab.beancipher.spi.IBeanDescriptor;
import ar.com.rab.beancipher.spi.IBeanMapping;
import ar.com.rab.beancipher.spi.IPropertyCipher;
import ar.com.rab.beancipher.spi.IPropertyDescriptor;
import ar.com.rab.beancipher.spi.ITypeCipherAdapter;

/**
 * Builds instances of <code>BeanMapping</code> class
 * with the correspondign mappings (className / BeanDescriptor)
 * 
 * @author Bajales Raul
 *
 */
public class BeanMappingBuilder {

    private static final String JAVA_LANG_OBJECT = "java.lang.Object";
    private static final boolean DEFAULT_USE_INHERITED_PROPERTIES = false;
    private static final String GETTER_PREFIX = "get";
    private static final String SETTER_PREFIX = "set";
    private CFGBeanMapping cfgBeanMapping;
    private CFGRegistry cfgRegistry;
    private SimpleClassPackageExplorer classPackageExplorer;
    private IBeanMapping beanMapping;

    /**
     * Constructor
     * @param cfgRegistry
     * @param cfgBeanMapping
     */
    public BeanMappingBuilder(CFGBeanMapping cfgBeanMapping, CFGRegistry cfgRegistry) {
        super();
        this.cfgBeanMapping = cfgBeanMapping;
        this.cfgRegistry = cfgRegistry;
        this.classPackageExplorer = new SimpleClassPackageExplorer();
    }

    /**
     * Builds the mapping (className, propertyCipher instance)
     */
    public void build() throws Exception {
        this.beanMapping = new BeanMapping();
        Map propertyCipherMapping = buildPropertyCipherMapFor(cfgRegistry);
        for (Iterator iter = this.cfgBeanMapping.getMappings().iterator(); iter.hasNext();) {
            CFGMapping cfgMapping = (CFGMapping) iter.next();
            String propertyCipherName = cfgMapping.getSpecificPropertyCipher();
            if (propertyCipherName == null) propertyCipherName = this.cfgBeanMapping.getDefaultPropertyCipher();
            IPropertyCipher propertyCipher = (IPropertyCipher)propertyCipherMapping.get(propertyCipherName);
            mapClasses(this.beanMapping, cfgMapping, propertyCipher);
        }
    }

    /**
     * @param beanMapping
     * @param cfgMapping
     * @param propertyCipher
     * @throws ClassNotFoundException
     */
    private void mapClasses(IBeanMapping beanMapping, CFGMapping cfgMapping, IPropertyCipher propertyCipher) throws ClassNotFoundException {
        String className = cfgMapping.getClassName();
        String packageName = cfgMapping.getPackageName();
        String implementsOrExtends = cfgMapping.getImplementsOrExtends();
        List beanClassNames = findBeanClassNamesFor(className, packageName, implementsOrExtends);
        for (Iterator iter = beanClassNames.iterator(); iter.hasNext();) {
            String beanClassName = (String) iter.next();
            IBeanDescriptor descriptor = new BeanDescriptor(beanClassName, propertyCipher);
            addPropertiesNamesFor(descriptor, cfgMapping.getPropertiesNames(), cfgMapping.getAllProperties());
            beanMapping.addMapping(descriptor);
        }
    }

    /**
     * Available combinations: 
     *    - classname
     *    - implementsOrExtends + packagename
     *    - packagename
     *  
     * @param className
     * @param packageName
     * @param implementsOrExtends
     * @return List
     * @throws ClassNotFoundException
     */
    private List findBeanClassNamesFor(String className, String packageName, String implementsOrExtends) throws ClassNotFoundException {
        List retValue = new ArrayList();
        if (className != null) 
            retValue.add(className);
        else if ((implementsOrExtends != null) && (packageName != null)) 
            retValue.addAll(findImplementationsOrExtensionsFor(findClassNamesFor(packageName), implementsOrExtends));
        else if (packageName != null) 
            retValue.addAll(findClassNamesFor(packageName));
        return retValue;
    }

    /**
     * @param classNames
     * @param className
     * @return List
     * @throws ClassNotFoundException
     */
    private List findImplementationsOrExtensionsFor(List classNames, String className) throws ClassNotFoundException {
    	List retValue = new ArrayList();
    	for (Iterator iter = classNames.iterator(); iter.hasNext();) {
            String classNameToTest = (String) iter.next();
            if (implementsOrExtends(classNameToTest, className)) retValue.add(classNameToTest);            
        }
        return retValue;
    }

    /**
     * @param classNameToTest
     * @param className
	 * @return boolean, true is classNameToTest extends or implements className
     * @throws ClassNotFoundException
     */
    private boolean implementsOrExtends(String classNameToTest, String className) throws ClassNotFoundException {
    	Class clazz = Class.forName(classNameToTest);
		while (clazz != null && !clazz.getName().equalsIgnoreCase(JAVA_LANG_OBJECT)) {
			if (clazz.getName().equals(className)) return true;
			for (int i = 0; i < clazz.getInterfaces().length; i++) 
				if (clazz.getInterfaces()[i].getName().equals(className)) return true;
			clazz = clazz.getSuperclass();
		}
        return false;
    }

    /**
     * @param packageName
     * @return List
     */
    private List findClassNamesFor(String packageName) {
    	return Arrays.asList(this.classPackageExplorer.listPackage(packageName));
    }

    /**
     * @param descriptor
     * @param propertiesNames
     * @param allProperties
     * @throws ClassNotFoundException
     */
    private void addPropertiesNamesFor(IBeanDescriptor descriptor, List propertiesNames, CFGAllProperties allProperties) throws ClassNotFoundException {
        if (allProperties != null) 
            addAllPropertiesFor(descriptor, allProperties);
        else {
            List propertiesDescriptorsToAdd = buildPropertiesDescriptors(descriptor, propertiesNames);
            descriptor.addAllPropertyDescriptors(propertiesDescriptorsToAdd);
        }
    }

    /**
     * @param descriptor
     * @param propertiesNames
     * @return
     * @throws ClassNotFoundException
     */
    private List buildPropertiesDescriptors(IBeanDescriptor descriptor, List propertiesNames) throws ClassNotFoundException {
        List propertiesDescriptorsToAdd = new ArrayList();
        List allPropertiesDescriptors = getAccesiblePropertiesFrom(descriptor.getBeanClassName(), Boolean.TRUE.toString());
        for (Iterator iter = allPropertiesDescriptors.iterator(); iter.hasNext();) {
            IPropertyDescriptor element = (IPropertyDescriptor) iter.next();
            if (propertiesNames.contains(new CFGProperty(element.getPropertyName()))) propertiesDescriptorsToAdd.add(element);
        }
        return propertiesDescriptorsToAdd;
    }

    /**
     * @param descriptor
     * @param allProperties
     * @throws ClassNotFoundException
     */
    private void addAllPropertiesFor(IBeanDescriptor descriptor, CFGAllProperties allProperties) throws ClassNotFoundException {
        List allPropertiesDescriptors = getAccesiblePropertiesFrom(descriptor.getBeanClassName(), allProperties.getUseInheriteds());
        List exclude = buildPropertiesDescriptors(descriptor, (allProperties.getExcludings() != null) ? allProperties.getExcludings() : new ArrayList());
        allPropertiesDescriptors.removeAll(exclude);
        descriptor.addAllPropertyDescriptors(allPropertiesDescriptors);
    }

    /**
     * @param beanClassName
     * @param useInheriteds
     * @return List
     * @throws ClassNotFoundException
     */
    private List getAccesiblePropertiesFrom(String beanClassName, String useInheriteds) throws ClassNotFoundException {
        boolean useInheritedsProperties = DEFAULT_USE_INHERITED_PROPERTIES;
        if ((useInheriteds != null) && (useInheriteds.equalsIgnoreCase(Boolean.TRUE.toString()))) useInheritedsProperties = true;
        List retValue = new ArrayList();
        Class clazz = this.getClass().getClassLoader().loadClass(beanClassName);
        Field[] fields = useInheritedsProperties ? findAllFieldsFor(clazz) : clazz.getDeclaredFields();
        Method[] methods = useInheritedsProperties ? clazz.getMethods() : clazz.getDeclaredMethods();
        retValue = findPropertyDescriptorsForAccesibleFields(fields, methods); 
        return retValue;
    }
    
    /**
     * @param clazz
     * @return
     */
    private Field[] findAllFieldsFor(Class clazz) {
        List list = new ArrayList();
		while (clazz != null && !clazz.getName().equalsIgnoreCase(JAVA_LANG_OBJECT)) {
		    list.addAll(Arrays.asList(clazz.getDeclaredFields()));
			clazz = clazz.getSuperclass();
		}
		Field[] retValue = new Field[list.size()];
		int index = 0;
		for (Iterator iter = list.iterator(); iter.hasNext();) 
		    retValue[index++] = (Field) iter.next();
        return retValue;
    }

    /**
     * @param fields
     * @param methods
     * @return List
     */
    private List findPropertyDescriptorsForAccesibleFields(Field[] fields, Method[] methods) {
        List retValue = new ArrayList();
        for (int i = 0; i < fields.length; i++) {
            Field field = fields[i];
            String fieldName = field.getName();
            String className = field.getType().getName();
			String fieldNameCapitalized = fieldName.substring(0,1).toUpperCase() + fieldName.substring(1, fieldName.length());
            String getterName = GETTER_PREFIX + fieldNameCapitalized;
			String setterName = SETTER_PREFIX + fieldNameCapitalized;
			Accesors accesors = findAccesors(getterName, setterName, methods);
			if (accesors.exists) 
			    retValue.add(new PropertyDescriptor(fieldName, className, accesors.setter, accesors.getter));
		}
        return retValue;
    }

    /**
     * @param getterName
     * @param setterName
     * @param methods
     * @return Accesors
     */
    private Accesors findAccesors(String getterName, String setterName, Method[] methods) {
        Method setter = null;
        Method getter = null;
        for (int i = 0; i < methods.length; i++) {
            Method method = methods[i];
            if ((method.getName().equals(getterName)) && (Modifier.isPublic(method.getModifiers()))) 
                getter = method; 
            if ((method.getName().equals(setterName)) && (Modifier.isPublic(method.getModifiers()))) 
                setter = method; 
        }
        return new Accesors(getter, setter, (setter != null) && (getter != null));
    }

    /**
     * @param cfgRegistry
     * @return Map
     * @throws Exception
     */
    private Map buildPropertyCipherMapFor(CFGRegistry cfgRegistry) throws Exception {
        Map map = new HashMap();
        for (Iterator iter = cfgRegistry.getPropertyCiphers().iterator(); iter.hasNext();) {
            CFGPropertyCipher cfgPropertyCipher = (CFGPropertyCipher) iter.next();
            ITypeCipherAdapter typeCipher = instantiateTypeCipherFor(cfgPropertyCipher.getDefaultTypeCipher());
            Map specificMapping = buildSpecificMappingFor(cfgPropertyCipher.getWhens());
            IPropertyCipher propertyCipher = new PropertyCipher(typeCipher, specificMapping);
            map.put(cfgPropertyCipher.getName(), propertyCipher);
        }
        return map;
    }

    /**
     * @param whens
     * @return Map
     * @throws Exception
     */
    private Map buildSpecificMappingFor(List whens) throws Exception {
        Map retValue = new HashMap();
        if (whens == null) return retValue;
        for (Iterator iter = whens.iterator(); iter.hasNext();) {
            CFGWhen when = (CFGWhen) iter.next();
            ITypeCipherAdapter typeCipher = instantiateTypeCipherFor(when.getUseTypeCipherImpl());
            retValue.put(when.getPropertyType(), typeCipher);
        }
        return retValue;
    }

    /**
     * @param typeCipherClassName
     * @return ITypeCipherAdapter
     * @throws Exception
     */
    private ITypeCipherAdapter instantiateTypeCipherFor(String typeCipherClassName) throws Exception {
        try {
            Class clazz = Class.forName(typeCipherClassName);
            return (ITypeCipherAdapter) clazz.newInstance();
        } catch (Exception e) {
            throw new Exception(e.getMessage(), e);
        }
    }

    /**
     * @return BeanMapping
     */
    public IBeanMapping get() {
        return this.beanMapping;
    }
    
    /**
     * @author Bajales Raul
     *
     */
    private class Accesors {
        
        /**
         * @param getter
         * @param setter
         * @param exists
         */
        public Accesors(Method getter, Method setter, boolean exists) {
            super();
            this.getter = getter;
            this.setter = setter;
            this.exists = exists;
        }
        
        public boolean exists = false;
        public Method getter = null;
        public Method setter = null;
    }

}
